/******************************************************************************
 * This program is a 100% Java Email Server.
 ******************************************************************************
 * Copyright (c) 2001-2011, Eric Daugherty (http://www.ericdaugherty.com)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   * Neither the name of the copyright holder nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ''AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 ******************************************************************************
 * For current versions and more information, please visit:
 * http://javaemailserver.sf.net/
 *
 * or contact the authors at:
 * java@ericdaugherty.com
 * andreaskyrmegalos@hotmail.com
 *
 ******************************************************************************
 * This program is based on the CSRMail project written by Calvin Smith.
 * http://crsemail.sourceforge.net/
 ******************************************************************************
 *
 * $Rev$
 * $Date$
 *
 ******************************************************************************/

package com.ericdaugherty.mail.server.services.smtp;

//Java Imports
import java.io.*;
import java.net.*;
import java.util.*;
import javax.net.ssl.SSLSocket;

//Log imports
import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;

//dnsjava imports
import org.xbill.DNS.*;

//Encoding imports
import org.apache.commons.codec.binary.Base64;

//Local Imports
import com.ericdaugherty.mail.server.info.EmailAddress;
import com.ericdaugherty.mail.server.configuration.ConfigurationManager;
import com.ericdaugherty.mail.server.configuration.DefaultSMTPServer;
import com.ericdaugherty.mail.server.errors.PermanentNegativeException;
import com.ericdaugherty.mail.server.errors.TransientNegativeException;
import com.ericdaugherty.mail.server.security.transport.TransportLayer;
import com.ericdaugherty.mail.server.services.smtp.support.FailedAddressItem;

/**
 * This class handles sending messages to external SMTP servers for delivery.
 *
 * @author Eric Daugherty
 * @author Andreas Kyrmegalos (2.x branch)
 */
public class SMTPRemoteSender extends MIMESender{

    /** Logger */
    private static final Log log = LogFactory.getLog( SMTPRemoteSender.class );

    /** ConfigurationManager */
    private final ConfigurationManager configurationManager = ConfigurationManager.getInstance();

    //***************************************************************
    // Variables
    //***************************************************************
    
    /** Writer to sent data to the client */
    private PrintWriter out;
    /** Reader to read data from the client */
    private BufferedReader in;

    /** Socket connection to the client */
    private Socket socket;

    /** The socket's outputStream **/
    private OutputStream rawOut;

    // Credentials for authentication with the default SMTP server
    private String username;
    private String password;
    
    private int session_stage = SESSION_PREBG;
    private boolean success, conclude, proceed, atLeastOneRCPTAccepted, mime8bit;
    private boolean ehlo = true;
    private Object[] capabilities;
    private Vector<String> tempresponse = new Vector<String>(0);
    private List<EmailAddress> originalAddresses;
    private List<EmailAddress> remainingAddresses;
    private List<FailedAddressItem> failedAddresses = new ArrayList<FailedAddressItem>();
    private Prepareaction prepareaction;

    //***************************************************************
    // Public Interface
    //***************************************************************

    public List<FailedAddressItem> getFailedAddresses() {
       return failedAddresses;
    }

    public void cleanUp() {
       tempresponse.clear();tempresponse = null;
       originalAddresses.clear();originalAddresses = null;
       remainingAddresses.clear();remainingAddresses = null;
       if (capabilities!=null) {
          for (int i=0;i<capabilities.length;i++) {
             capabilities[i] = null;
          }
          capabilities = null;
       }
       failedAddresses.clear();failedAddresses = null;
    }

    //***************************************************************
    // Constructor(s)

    public SMTPRemoteSender() {}

    //***************************************************************
    // Methods

    /**
     * Handles delivery of messages to addresses not handled by this server.
     */
    public void sendMessage( EmailAddress address, SMTPMessage message ) throws TransientNegativeException, PermanentNegativeException {
       String remoteDomain = address.getDomain();
       List<EmailAddress> addresses = new ArrayList<EmailAddress>(1);
       addresses.add(address);
       sendMessage(null, remoteDomain, addresses, message);
    }
    public final void sendMessage( String remoteDomain, List<EmailAddress> addresses, SMTPMessage message )
          throws TransientNegativeException, PermanentNegativeException {
       sendMessage(null, remoteDomain, addresses, message);
    }
    protected void sendMessage( Socket socketPre, String remoteDomain, List<EmailAddress> addresses, SMTPMessage message )
          throws TransientNegativeException, PermanentNegativeException {

       originalAddresses = new ArrayList<EmailAddress>(addresses);
       remainingAddresses = new ArrayList<EmailAddress>(addresses);

       StringBuilder localDomain = new StringBuilder(message.getFromAddress().getDomain());
       //Provision for proper identification of mail sender when on a dynamic DNS where the mail receiver uses reverseDNS
       //to identify the sender
       if (isreverseDNSserver(remoteDomain)) {
          try {
             Record[] records = new Lookup(message.getFromAddress().getDomain(), Type.A).run();
             localDomain = new StringBuilder("[").append(((ARecord)records[0]).getAddress().getHostAddress()).append("]");
          } catch (Exception ex) {
             log.error( ex.getMessage() );
             //If there is an error here it's better ro retry sending the message.
             //It most likely has to do with temporary improper lookup results.
             throw new TransientNegativeException( "Unable to complete the transaction due to DNS lookup error.", null );
          }
       }
       
        //Open the connection to the server.
        if (socketPre == null) {
           try {
              socket = connect( remoteDomain );
           }
           catch(PermanentNegativeException pne) {
              while(remainingAddresses.size()>0) {
                 failedAddresses.add(new FailedAddressItem(remainingAddresses.remove(0), "554 "+pne.getLocalizedMessage()));
              }
              throw pne;
           }
        }
        else {
           socket = socketPre;
        }

        try {
            reset();
            //Get the input and output streams.
            rawOut = socket.getOutputStream();
            out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(rawOut, US_ASCII)));
            super.setPrintOut(out);
            in = new BufferedReader(new InputStreamReader(socket.getInputStream(), US_ASCII));
            
            Waitforconnection();
            
            processreply();
            if (prepareaction.resend) {
               throw new TransientNegativeException( new StringBuilder(prepareaction.replycode).append(prepareaction.replyargument).toString(), null );
            }
            do {
               sendcommand( localDomain, message );
               processreply();
               if (conclude) {
                  if (success) break;
                  else {
                     StringBuilder exceptionmessage =
                           new StringBuilder(prepareaction.replycode).append(prepareaction.replyargument);
                     if (prepareaction.resend) {
                        throw new TransientNegativeException( exceptionmessage.toString(), null );
                     }
                     else {
                        throw new PermanentNegativeException( exceptionmessage.toString() );
                     }
                  }
               }
            }while(true);
        }
        catch (IOException ioe) {
           log.error( ioe.getMessage() );
           throw new TransientNegativeException( "Unable to complete the transaction due to internal error.", ioe );
        }
        finally {
            if (in != null) {
               try {
                  in.close();
               }
               catch( IOException ioe ) {
                  log.error( "Error closing input stream: " + ioe );
               }
               in = null;
            }
            if (out != null) {
               out.close();
               out = null;
            }
            if( socket != null )  {
               try {
                  socket.close();
               }
               catch( IOException ioe ) {
                  log.error( "Error closing socket: " + ioe );
               }
               socket = null;
            }
        }
    }

    //***************************************************************
    // Private Interface
    //***************************************************************

    private void Waitforconnection() throws SocketException, TransientNegativeException{
       int count = 0;
       while (!socket.isConnected()) {
          try {
             Thread.sleep(500);
          } catch (InterruptedException ex) {
             log.error( ex.getMessage() );
             throw new TransientNegativeException( "Unable to complete the transaction due to internal error.", null );
          }
          if (count++> 120) throw new TransientNegativeException( "No connection established. Retrying later.", null );
       }
       socket.setSoTimeout(5 * 60 * 1000);
    }
    
    private void reset() {
       prepareaction = new Prepareaction();
       session_stage = SESSION_PREBG;
       success = false;
       conclude = false;
       proceed = false;
       mime8bit = false;
       atLeastOneRCPTAccepted = false;
       ehlo = true;
       capabilities = null;
       tempresponse = new Vector<String>(4);
       failedAddresses = new ArrayList<FailedAddressItem>();
       remainingAddresses = new ArrayList<EmailAddress>(originalAddresses);
       initialHeaders = true;
       readingHeaders = false;
       convertNextPart = false;
       if (boundaries!=null) boundaries.clear();
       boundaries = new ArrayList<String>();
       mime = MIME_UNDEFINED;
       emptyStringCount = currentRead = previousRead = currentTotal = 0;
       if (bufferForb64os != null) {
          for (int i=bufferForb64os.length-1;i>=0;i--) {
             bufferForb64os[i] = 0;
          }
          bufferForb64os = null;
       }
    }
    
    private void sendcommand( StringBuilder localDomain,
          SMTPMessage message ) throws SocketException, IOException{
       switch (session_stage) {
          case SESSION_EHLO: {
             if (ehlo) write( "EHLO " + localDomain );
             else write( "HELO " + localDomain );
             socket.setSoTimeout(5 * 60 * 1000);
          }break;
          case SESSION_MAIL: {
             if (ehlo) {
                if (supportedcapability("8BITMIME")) {
                   mime8bit = true;
                }
                if (supportedcapability("SIZE")) {
                   write( "MAIL FROM:<" + message.getFromAddress().getAddress() + "> SIZE="+(message.getSMTPPersistenceProccessor().getPersistedSize()-20L) );
                   socket.setSoTimeout(5 * 60 * 1000);
                   return;
                }
             }
             write( "MAIL FROM:<" + message.getFromAddress().getAddress() + ">" );
             socket.setSoTimeout(5 * 60 * 1000);
          }break;
          case SESSION_RCPT: {
             write( "RCPT TO:<" + remainingAddresses.get(0) + ">" );
             socket.setSoTimeout(5 * 60 * 1000);
          }break;
          case SESSION_DATA: {
             if (!proceed) {
                write( "DATA" );
                socket.setSoTimeout(2 * 60 * 1000);
             }
             else {
                sendData( message );
                socket.setSoTimeout(10 * 60 * 1000);
             }
          }break;
          case SESSION_QUIT: {
             conclude = true;
             write("QUIT");
             socket.setSoTimeout(5 * 60 * 1000);

          }break;
          case SESSION_RESET: {
             write( "RSET" );
             socket.setSoTimeout(5 * 60 * 1000);
          }
       }
    }
    
    private void authenticate() throws TransientNegativeException, SocketException, SocketTimeoutException{
       
       if (supportedcapability("STARTTLS")) {
          if (configurationManager.isOutgoingSecure()) {
             write( "STARTTLS" );
             socket.setSoTimeout(5 * 60 * 1000);
             String replycode = read();
             if (replycode.length() != 0) {
                char firstdigit = replycode.charAt(0);
                if (firstdigit == TWO) {
                   TransportLayer transportLayer = new TransportLayer();
                   try {
                      boolean acceptable = false;
                      transportLayer.init(socket,false,true);
                      String cipher = ((SSLSocket)transportLayer.getSocket()).getSession().getCipherSuite();
                      log.info("Negotiated Cipher: "+cipher);
                      String[] ec = configurationManager.getEnabledCiphers();
                      for (int i=0;i<ec.length;i++) {
                         if (cipher.equals(ec[i])) {
                            acceptable = true;
                            break;
                         }
                      }
                      if (!acceptable) {
                         session_stage = SESSION_RESET;
                         prepareaction.setup(replycode, true);
                         return;
                      }
                      socket = transportLayer.getSocket();
                      out.flush();
                      rawOut = socket.getOutputStream();
                      out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(rawOut, US_ASCII)));
                      super.setPrintOut(out);
                      in = new BufferedReader(new InputStreamReader(socket.getInputStream(), US_ASCII));
                      session_stage = SESSION_EHLO;
                   } catch (Exception ex) {
                      //Per RFC 3207:
                      //If, after having issued the STARTTLS command, the client finds out that
                      //some failure prevents it from actually starting a TLS handshake, then
                      //it SHOULD abort the connection.
                      throw new TransientNegativeException(ex.getMessage(), ex);
                   }
                   finally {
                      transportLayer.conclude();
                   }
                }
                else if (firstdigit == FOUR) {
                   session_stage = SESSION_RESET;
                   prepareaction.setup(replycode, true);
                }
                else if (firstdigit == FIVE) {
                   session_stage = SESSION_RESET;
                   prepareaction.setup(replycode, false);
                }
                else {
                   session_stage = SESSION_RESET;
                   prepareaction.setup(replycode, false);
                }
             }
             else {
                session_stage = SESSION_QUIT;
                prepareaction.setup(replycode, false);
             }
          }
          else if (username!=null) {
              authenticateCredentials();
          }
          else {
             session_stage = SESSION_MAIL;
          }
       }
       else if (username!=null) {
           authenticateCredentials();
       }
       else {
          session_stage = SESSION_MAIL;
       }
    }
    
    private void authenticateCredentials() throws TransientNegativeException, SocketException, SocketTimeoutException{
          if (supportedcapability("PLAIN")) {
             write( "AUTH PLAIN "+new String(Base64.encodeBase64(("\u0000"+username+"\u0000"+password).getBytes())) );
             socket.setSoTimeout(2 * 60 * 1000);
             String replycode = read();
             if (replycode.length() != 0) {
                char firstdigit = replycode.charAt(0);
                if (firstdigit == TWO) {
                   session_stage = SESSION_MAIL;
                   return;
                }
                else if (firstdigit == THREE) {
                   write( new String(Base64.encodeBase64(("\u0000"+username+"\u0000"+password).getBytes())) );
                   socket.setSoTimeout(2 * 60 * 1000);
                   replycode = read();
                   if (replycode.length() != 0) {
                      firstdigit = replycode.charAt(0);
                      if (firstdigit == TWO) {
                         session_stage = SESSION_MAIL;
                         return;
                      }
                   }
                }
                session_stage = SESSION_RESET;
                prepareaction.setup(replycode, false);
             }
             session_stage = SESSION_QUIT;
             prepareaction.setup(replycode, false);
          }
          else if (supportedcapability("LOGIN")) {
             write( "AUTH LOGIN" );
             socket.setSoTimeout(2 * 60 * 1000);
             String replycode = read();
             if (replycode.length() != 0) {
                char firstdigit = replycode.charAt(0);
                if (firstdigit == THREE) {
                   write( new String(Base64.encodeBase64(username.getBytes())) );
                   socket.setSoTimeout(2 * 60 * 1000);
                   replycode = read();
                   if (replycode.length() != 0) {
                      firstdigit = replycode.charAt(0);
                      if (firstdigit == THREE) {
                         write( new String(Base64.encodeBase64(password.getBytes())) );
                         socket.setSoTimeout(2 * 60 * 1000);
                         replycode = read();
                         if (replycode.length() != 0) {
                            firstdigit = replycode.charAt(0);
                            if (firstdigit == TWO) {
                               session_stage = SESSION_MAIL;
                               return;
                            }
                         }
                      }
                   }
                }
                session_stage = SESSION_RESET;
                prepareaction.setup(replycode, false);
             }
             session_stage = SESSION_QUIT;
             prepareaction.setup(replycode, false);
          }
    }
    
    private class Prepareaction {
       
       boolean resend;
       String replycode = "";
       String replyargument = "";
       
       private void setup(String replycode, boolean resend) {
          this.resend = replycode.equals("421") ? true:resend;
          this.replycode = replycode;
          try {
             replyargument = " "+tempresponse.firstElement().toString();
          } 
          catch (Exception ex) {
             //Just continue
          }
       }
    }
    
    private void processreply() throws TransientNegativeException, SocketException, SocketTimeoutException{
       String replyCode = null;
       try {
          replyCode = read();
       }
       catch (TransientNegativeException tne) {
          while(remainingAddresses.size()>0) {
             failedAddresses.add(new FailedAddressItem(remainingAddresses.remove(0), "451 "+(tne.getException()!=null?tne.getException().getMessage():"")));
          }
          throw tne;
       }
       if (replyCode.length() != 0) {
          char firstdigit = replyCode.charAt(0);
          switch (session_stage) {
             case SESSION_PREBG: {
                if (firstdigit == TWO) {
                   session_stage = SESSION_EHLO;
                }
                else if (firstdigit == FIVE) {
                   session_stage = SESSION_QUIT;
                   prepareaction.setup(replyCode, false);
                }
                else {
                   session_stage = SESSION_QUIT;
                   prepareaction.setup(replyCode, false);
                }
             }break;
             case SESSION_EHLO: {
                if (firstdigit == TWO) {
                   if (ehlo) {
                      capabilities = tempresponse.toArray();
                      authenticate();
                   }
                   else {
                      session_stage = SESSION_MAIL;
                   }
                }
                else if (firstdigit == FIVE) {
                   char seconddigit = replyCode.charAt(1);
                   if (seconddigit == ZERO) {
                      char thirddigit = replyCode.charAt(2);
                      if (thirddigit == FOUR) {
                         ehlo = false;
                      }
                      else {
                         session_stage = SESSION_QUIT;
                         prepareaction.setup(replyCode, false);
                      }
                   }
                   else if (seconddigit == FIVE) {
                      char thirddigit = replyCode.charAt(2);
                      if (thirddigit == ZERO) {
                         session_stage = SESSION_RESET;
                         prepareaction.setup(replyCode, false);
                      }
                      else {
                         session_stage = SESSION_QUIT;
                         prepareaction.setup(replyCode, false);
                      }
                   }
                   else {
                      session_stage = SESSION_QUIT;
                      prepareaction.setup(replyCode, false);
                   }
                }
                else {
                   session_stage = SESSION_QUIT;
                   prepareaction.setup(replyCode, false);
                }
             }break;
             case SESSION_MAIL: {
                if (firstdigit == TWO) {
                   session_stage = SESSION_RCPT;
                }
                else if (firstdigit == FOUR) {
                   char seconddigit = replyCode.charAt(1);
                   if (seconddigit == FIVE) {
                      char thirddigit = replyCode.charAt(2);
                      if (thirddigit == ONE) {
                         session_stage = SESSION_RESET;
                         prepareaction.setup(replyCode, true);
                      }
                      else if (thirddigit == TWO) {
                         session_stage = SESSION_RESET;
                         prepareaction.setup(replyCode, true);
                      }
                      else {
                         session_stage = SESSION_QUIT;
                         prepareaction.setup(replyCode, true);
                      }
                   }
                   else {
                      session_stage = SESSION_QUIT;
                      prepareaction.setup(replyCode, true);
                   }
                }
                //Implies permanent negative completion reply. Some mail receiving servers reject dynamic ip ranges at this point.
                else {
                   String response = replyCode+" "+(String)tempresponse.get(0);
                   while(remainingAddresses.size()>0) {
                      failedAddresses.add(new FailedAddressItem(remainingAddresses.remove(0), response));
                   }
                   session_stage = SESSION_QUIT;
                   prepareaction.setup(replyCode, false);
                }
             }break;
             case SESSION_RCPT: {
                if (firstdigit == TWO) {
                   char seconddigit = replyCode.charAt(1);
                   if (seconddigit == FIVE) {
                      char thirddigit = replyCode.charAt(2);
                      if (thirddigit == ZERO) {
                         atLeastOneRCPTAccepted = true;
                         remainingAddresses.remove(0);
                         if (remainingAddresses.isEmpty()) {
                            session_stage = SESSION_DATA;
                         }
                         else {
                            session_stage = SESSION_RCPT;
                         }
                      }
                      else {
                         processReply0(SESSION_QUIT, replyCode, false);
                      }
                   }
                   else {
                      processReply0(SESSION_QUIT, replyCode, false);
                   }
                }
                else if (firstdigit == FOUR) {
                   char seconddigit = replyCode.charAt(1);
                   if (seconddigit == FIVE) {
                      char thirddigit = replyCode.charAt(2);
                      if (thirddigit == ZERO) {
                         processReply0(SESSION_RESET, replyCode, true);
                      }
                      else if (thirddigit == ONE) {
                         processReply0(SESSION_RESET, replyCode, true);
                      }
                      else if (thirddigit == TWO) {
                         processReply0(SESSION_RESET, replyCode, true);
                      }
                      else {
                         processReply0(SESSION_QUIT, replyCode, true);
                      }
                   }
                   else {
                      processReply0(SESSION_QUIT, replyCode, true);
                   }
                }
                else if (firstdigit == FIVE) {
                   char seconddigit = replyCode.charAt(1);
                   if (seconddigit == FIVE) {
                      char thirddigit = replyCode.charAt(2);
                      if (thirddigit == ZERO) {
                         processReply0(SESSION_RESET, replyCode, false);
                      }
                      else if (thirddigit == ONE) {
                         processReply0(SESSION_RESET, replyCode, false);
                      }
                      else if (thirddigit == TWO) {
                         processReply0(SESSION_RESET, replyCode, false);
                      }
                      else if (thirddigit == THREE) {
                         processReply0(SESSION_RESET, replyCode, false);
                      }
                      else {
                         processReply0(SESSION_QUIT, replyCode, false);
                      }
                   }
                   else if (seconddigit == ZERO) {
                      char thirddigit = replyCode.charAt(2);
                      if (thirddigit == THREE) {
                         processReply0(SESSION_RESET, replyCode, false);
                      }
                      else {
                         processReply0(SESSION_QUIT, replyCode, false);
                      }
                   }
                   else {
                      processReply0(SESSION_QUIT, replyCode, false);
                   }
                }
                else {
                   processReply0(SESSION_QUIT, replyCode, false);
                }
             }break;
             case SESSION_DATA: {
                if (!proceed) {
                   if (firstdigit == THREE) {
                      proceed = true;
                   }
                   else if (firstdigit == FOUR) {
                      char seconddigit = replyCode.charAt(1);
                      if (seconddigit == FIVE) {
                         char thirddigit = replyCode.charAt(2);
                         if (thirddigit == ONE) {
                            session_stage = SESSION_RESET;
                            prepareaction.setup(replyCode, true);
                         }
                         else {
                            session_stage = SESSION_QUIT;
                            prepareaction.setup(replyCode, true);
                         }
                      }
                      else {
                         session_stage = SESSION_QUIT;
                         prepareaction.setup(replyCode, true);
                      }
                   }
                   else if (firstdigit == FIVE) {
                      char seconddigit = replyCode.charAt(1);
                      if (seconddigit == FIVE) {
                         char thirddigit = replyCode.charAt(2);
                         if (thirddigit == FOUR) {
                            session_stage = SESSION_RESET;
                            prepareaction.setup(replyCode, false);
                         }
                         else {
                            session_stage = SESSION_QUIT;
                            prepareaction.setup(replyCode, false);
                         }
                      }
                      else if (seconddigit == ZERO) {
                         char thirddigit = replyCode.charAt(2);
                         if (thirddigit == THREE) {
                            session_stage = SESSION_RESET;
                            prepareaction.setup(replyCode, false);
                         }
                         else {
                            session_stage = SESSION_QUIT;
                            prepareaction.setup(replyCode, false);
                         }
                      }
                   }
                   else {
                      session_stage = SESSION_QUIT;
                      prepareaction.setup(replyCode, false);
                   }
                }
                else {
                   if (firstdigit == TWO) {
                      success = true;
                      session_stage = SESSION_QUIT;
                   }
                   else if (firstdigit == FOUR) {
                      char seconddigit = replyCode.charAt(1);
                      if (seconddigit == FIVE) {
                         char thirddigit = replyCode.charAt(2);
                         if (thirddigit == ONE) {
                            session_stage = SESSION_RESET;
                            prepareaction.setup(replyCode, true);
                         }
                         else if (thirddigit == TWO) {
                            session_stage = SESSION_RESET;
                            prepareaction.setup(replyCode, true);
                         }
                         else {
                            session_stage = SESSION_QUIT;
                            prepareaction.setup(replyCode, true);
                         }
                      }
                      else {
                         session_stage = SESSION_QUIT;
                         prepareaction.setup(replyCode, true);
                      }
                   }
                   else if (firstdigit == FIVE) {
                      char seconddigit = replyCode.charAt(1);
                      if (seconddigit == FIVE) {
                         char thirddigit = replyCode.charAt(2);
                         if (thirddigit == TWO) {
                            session_stage = SESSION_RESET;
                            prepareaction.setup(replyCode, false);
                         }
                         else if (thirddigit == FOUR) {
                            session_stage = SESSION_RESET;
                            prepareaction.setup(replyCode, false);
                         }
                         else {
                            session_stage = SESSION_QUIT;
                            prepareaction.setup(replyCode, false);
                         }
                      }
                      else {
                         session_stage = SESSION_QUIT;
                         prepareaction.setup(replyCode, false);
                      }
                   }
                   else {
                      session_stage = SESSION_QUIT;
                      prepareaction.setup(replyCode, false);
                   }
                }
             }break;
             case SESSION_QUIT: {
                if (firstdigit != TWO) {
                   success = false;
                }
                log.info("Session has ended");
             }break;
             case SESSION_RESET: {
                success = false;
                session_stage = SESSION_QUIT;
             }break;
          }
       }
       else if (session_stage != SESSION_QUIT) {
          session_stage = SESSION_QUIT;
          prepareaction.setup(replyCode, false);
       }
    }

    private void processReply0(int nextStage, String replyCode, boolean reSend) {

       failedAddresses.add(new FailedAddressItem(remainingAddresses.remove(0), replyCode+" "+(String)tempresponse.get(0)));
       if (remainingAddresses.isEmpty() && atLeastOneRCPTAccepted) {
          session_stage = SESSION_DATA;
       }
       else if (remainingAddresses.size()>0) {
          session_stage = SESSION_RCPT;
       }
       else {
          session_stage = nextStage;
          prepareaction.setup(replyCode, reSend);
       }
    }
    
    private boolean supportedcapability(String input) {
       for (int i=capabilities.length-1;i>=0;i--) {
          if (((String)capabilities[i]).toUpperCase(locale).contains(input))
             return true;
       }
       return false;
    }
    
    /**
     * Determines the MX entries for this domain and attempts to open
     * a socket.  If no connections can be opened, a SystemException is thrown.
     */
    protected Socket connect( String domain ) throws PermanentNegativeException {

       if (domain.equals("example.com")) {
          try {
             return new Socket((String)null, configurationManager.getSMTPPort()+1);
          }
          catch( Exception e ) {
             log.error( "Connection to SMTP Server: example.com failed with exception: " + e ) ;
             throw new PermanentNegativeException( "Could not connect to any SMTP server for domain example.com" );
          }
       }
        String[] mxEntries = null;

        //Check to see if a default smtp server is configured before performing
        //the DNS lookup.
        if( configurationManager.isDefaultSmtpServerEnabled() )
        {
            DefaultSMTPServer[] defaultMXEntries = configurationManager.getDefaultSmtpServers();
            for( int index = 0; index < defaultMXEntries.length; index++ ) {

                DefaultSMTPServer mxEntry = defaultMXEntries[index];
                try {
                    username = mxEntry.getUsername();
                    password = mxEntry.getPassword();
                    return new Socket( mxEntry.getHost(), mxEntry.getPort() );
                }
                catch( Exception e ) {
                    log.error( "Connection to SMTP Server: " + mxEntry + " failed with exception: " + e ) ;
                }
            }
        }
        else {
            try {
                // Lookup the MX Entries

                // Doing a general lookup, clear DNS passwords.
                username = null;
                password = null;
                
                Record [] records = new Lookup(domain, Type.MX).run();
                if( records == null ) {
                    records = new MXRecord[0];
                    log.warn( "DNS Lookup for domain: " + domain + " failed." );
                }

                // Convert the MX Entries to strings and sort them in order
                // of priority.
                mxEntries = new String[records.length];
                int priority = 0;
                int nextPriority = Short.MAX_VALUE;
                int mxIndex = 0;
                while( mxIndex < mxEntries.length )
                {
                    for (int i = 0; i < records.length; i++) {
                        MXRecord mx = (MXRecord)records[i];
                        if( mx.getPriority() == priority )
                        {
                            mxEntries[mxIndex++] = mx.getTarget().toString();
                            if(mxIndex >= mxEntries.length) break;
                        }
                        else if( mx.getPriority() < nextPriority && mx.getPriority() > priority )
                        {
                            nextPriority = mx.getPriority();
                        }
                    }
                    priority = nextPriority;
                    nextPriority = Short.MAX_VALUE;
                }
            }
            catch( TextParseException e ) {
                throw new PermanentNegativeException( "Invalid domain name: "+domain );
            }

        }

        for( int index = 0; index < mxEntries.length; index++ ) {

            String mxEntry = mxEntries[index];
            int port = 25;

            // Extract the server and the port if the syntax server:port is used
            int indexPort = mxEntry.indexOf(':');
            if (indexPort >= 0) {
                try {
                    port = Integer.parseInt(mxEntry.substring(indexPort+1));
                }
                catch( Exception e ) {
                    log.error("Invalid defaultsmtpserver port: "+mxEntry.substring(indexPort+1)+" - "+e);
                }
                if (indexPort==0) {
                    mxEntry = "localhost";
                    mxEntries[index] = mxEntry + mxEntries[index];
                }
                else {
                    mxEntry = mxEntry.substring(0, indexPort);
                }
            }

            try {
                return new Socket( mxEntry, port );
            }
            catch( Exception e ) {
                log.error( "Connection to SMTP Server: " + mxEntries[index] + " failed with exception: " + e ) ;
            }
        }
        throw new PermanentNegativeException( "Could not connect to any SMTP server for domain: " + domain );
    }
    
    /**
     * This method searches for the server in the reverse DNS servers file
     * 
     * @param domain the RCPT TO: server domain
     * @return true if the server is in the reverse DNS servers file
     */
    private boolean isreverseDNSserver(String domain) {
       if (configurationManager.getReverseDNSservers()!=null) {
          String[] servers = configurationManager.getReverseDNSservers();
          int length = servers.length;
          String checkserver = domain.toUpperCase(locale);
          for (int i=0;i<length;i++) {
             if (checkserver.contains(servers[i].toUpperCase(locale))) {
                return true;
             }
          }
       }
       return false;
    }

   /**
    * This method sends the DATA only part of the message to the remote server.
    */
   protected void sendData( SMTPMessage message) throws SocketException, IOException{

      int count = 8;
      List<byte[]> dataLines = null;
      try {
         dataLines = message.getSMTPPersistenceProccessor().loadIncrementally(count);
         int numDataLines = Math.min(30,dataLines.size());

         boolean foundRPLCRCPT = false, foundRPLCID = false;

         String iterString;
         for (int iterCount = 0; iterCount< numDataLines; iterCount++) {
            iterString = new String(dataLines.get(iterCount),US_ASCII);
            if (iterString.indexOf("<REPLACE-RCPT>")!=-1) {
               iterString = "        for <" + (originalAddresses.get(0)).getAddress() + ">"+iterString.substring(iterString.indexOf(';'));
               dataLines.set(iterCount, iterString.getBytes(US_ASCII));
               foundRPLCRCPT = true;
            }
            else if (iterString.indexOf("<REPLACE-ID>")!=-1) {
               iterString = iterString.substring(0,iterString.indexOf('<'))+message.getSMTPUID()+
                     (iterString.charAt(iterString.length()-1)==';'?";":"");
               dataLines.set(iterCount, iterString.getBytes(US_ASCII));
               foundRPLCID = true;
            }
            if (foundRPLCRCPT&&foundRPLCID) break;
         }

         out.flush();

         if (!mime8bit&&message.is8bitMIME()) {

            long start = System.nanoTime();
            int dataSize = 0, position, index;
            byte[] singleLine;
            if(message.getSMTPPersistenceProccessor().isNotSavedInAmavis()&&configurationManager.isAmavisSupportActive()) {
               while (dataLines.size()>0) {
                  numDataLines = dataLines.size();
                  //Write the data.
                  for( index = 0; index < numDataLines; index++ ) {
                     singleLine = dataLines.get(index);
                     processDATA( singleLine, false, rawOut);
                     dataSize += singleLine.length+2;
                     if (log.isDebugEnabled()) {
                        if (System.nanoTime()-start>5000000000L) {
                           start = System.nanoTime();
                           log.debug(message.getSMTPUID()+" data sent: "+(dataSize/1024)+" KiB");
                        }
                     }
                  }
                  count+=250;
                  dataLines.clear();
                  dataLines = message.getSMTPPersistenceProccessor().loadIncrementally(count);
               }
            }
            else {
               while (dataLines.size()>0) {
                  numDataLines = dataLines.size();
                  //Write the data.
                  for( index = 0; index < numDataLines; index++ ) {
                     singleLine = dataLines.get( index );
                     position = singleLine.length;
                     //Provision for transparency according to RFC 2821/4.5.2
                     processDATA( singleLine, position>0&&singleLine[0]==0x2e, rawOut);
                     dataSize += singleLine.length+2;
                     if (log.isDebugEnabled()) {
                        if (System.nanoTime()-start>5000000000L) {
                           start = System.nanoTime();
                           log.debug(message.getSMTPUID()+" data sent: "+(dataSize/1024)+" KiB");
                        }
                     }
                  }
                  count+=250;
                  dataLines.clear();
                  dataLines = message.getSMTPPersistenceProccessor().loadIncrementally(count);
               }
            }
         }
         else {
            BufferedOutputStream out = new BufferedOutputStream(rawOut,4096);
            long start = System.nanoTime();
            int dataSize = 0, position, index;
            byte[] singleLine;
            if(message.getSMTPPersistenceProccessor().isNotSavedInAmavis()&&configurationManager.isAmavisSupportActive()) {
               while (dataLines.size()>0) {
                  numDataLines = dataLines.size();
                  //Write the data.
                  for( index = 0; index < numDataLines; index++ ) {
                     singleLine = dataLines.get( index );
                     out.write( singleLine );
                     out.write( CRLF_BYTES );
                     out.flush();
                     dataSize += singleLine.length+2;
                     if (log.isDebugEnabled()) {
                        if (System.nanoTime()-start>5000000000L) {
                           start = System.nanoTime();
                           log.debug(message.getSMTPUID()+" data sent: "+(dataSize/1024)+" KiB");
                        }
                     }
                  }
                  count+=250;
                  dataLines.clear();
                  dataLines = message.getSMTPPersistenceProccessor().loadIncrementally(count);
               }
            }
            else {
               while (dataLines.size()>0) {
                  numDataLines = dataLines.size();
                  //Write the data.
                  for( index = 0; index < numDataLines; index++ ) {
                     singleLine = dataLines.get( index );
                     position = singleLine.length;
                     //Provision for transparency according to RFC 2821/4.5.2
                     if (position>0&&singleLine[0]==0x2e) {
                        out.write( new byte[]{0x02e} );
                     }
                     out.write( singleLine );
                     out.write( CRLF_BYTES );
                     out.flush();
                     dataSize += singleLine.length;
                     if (log.isDebugEnabled()) {
                        if (System.nanoTime()-start>5000000000L) {
                           start = System.nanoTime();
                           log.debug(message.getSMTPUID()+" data sent: "+(dataSize/1024)+" KiB");
                        }
                     }
                  }
                  count+=250;
                  dataLines.clear();
                  dataLines = message.getSMTPPersistenceProccessor().loadIncrementally(count);
               }
            }
            //Send the command end data transmission.
            out.write(new byte[]{0x2e,0x0d,0x0a});
            out.flush();
         }
      }
      finally {
         if (dataLines!=null) {
            dataLines.clear();
            dataLines = null;
         }
      }
   }

    /**
     * Returns the response code generated by the server.
     * This method will handle multi-line responses, but will
     * only log the responses, and discard the text, returning
     * only the 3 digit response code.
     *
     * @return 3 digit response string.
     */
    private String read() throws TransientNegativeException {
        try {
            String responseCode;

            //Read in the first line.  This is the only line
            //we really care about, since the response code
            //must be the same on all lines.
            String inputText = in.readLine();
            if( inputText == null ) {
                inputText = "";
            }
            else {
                inputText = inputText.trim();
            }
            
            if( log.isDebugEnabled() ) { log.debug( "Read Input: " + inputText ); }
            if( inputText.length() < 3 ) {
                if (session_stage == SESSION_QUIT) {
                    return "";
                }
                else {
                   throw new TransientNegativeException( "SMTP Response too short. Aborting Send. Response: " + inputText, null );
                }
            }

            //Strip off the response code.
            responseCode = inputText.substring( 0, 3 );
            tempresponse.clear();
            tempresponse = new Vector<String>(4);
            if (responseCode.charAt(0)=="4".charAt(0)||responseCode.charAt(0)=="5".charAt(0)) {
               tempresponse.add(inputText.substring(4));
            }
            read0(inputText.substring(4));
            //Handle Multi-Line Responses.
            while( ( inputText.length() >= 4 ) && inputText.substring( 3, 4 ).equals( "-" ) ) {
                inputText = in.readLine().trim();
                read0(inputText.substring(4));
                if( log.isDebugEnabled() ) { log.debug( "Read Input: " + inputText ); }
            }

            return responseCode;
        }
        catch( IOException ioe ) {
            log.error( "Error reading from socket.", ioe );
            throw new TransientNegativeException( "Error reading from socket.", ioe );
        }
    }
    
    private void read0(String input) {

       String inputtUC = input.toUpperCase(locale);
       if (inputtUC.startsWith("AUTH")) {
          StringTokenizer st = new StringTokenizer(input, " ");
          st.nextToken();
          while (st.hasMoreTokens()) {
             tempresponse.add(st.nextToken());
          }
          return;
       }

       int stop = input.indexOf(' ');
       if (stop == -1) {
          tempresponse.add(input);
       }
       else {
          if (inputtUC.startsWith("HELLO")) {}
          else {
             tempresponse.add(input.substring(0, stop));
          }
       }
    }

    /**
     * Writes the specified output message to the client.
     */
    private void write( String message ) {
        if (message!=null) {
            if( log.isDebugEnabled() ) { log.debug( "Writing: " + message ); }
            out.print( message + CRLF_STRING );
            out.flush();
        }
    }
    
    private static final int SESSION_PREBG = -1;
    private static final int SESSION_EHLO = 0;
    private static final int SESSION_MAIL = 1;
    private static final int SESSION_RCPT = 2;
    private static final int SESSION_DATA = 3;
    private static final int SESSION_QUIT = 4;
    private static final int SESSION_RESET = 5;
    
    private static final int ZERO = 48;
    private static final int ONE = 49;
    private static final int TWO = 50;
    private static final int THREE = 51;
    private static final int FOUR = 52;
    private static final int FIVE = 53;

}
//EOF